Android OpenGL ES视频渲染(一)GLSurfaceView

您所在的位置:网站首页 linux 直接渲染视频 Android OpenGL ES视频渲染(一)GLSurfaceView

Android OpenGL ES视频渲染(一)GLSurfaceView

2024-07-09 10:53| 来源: 网络整理| 查看: 265

相关文章:Android OpenGL ES视频渲染(二)EGL+OpenGL

Android中视频渲染有几种方式,之前的文章使用的是nativewindow(包括softwareRender)。今天介绍另一总视频渲染的方式——OpenGL ES。 阅读本文之前需要对OpenGL有一定的了解,可以参考https://www.jianshu.com/p/99daa25b4573

在Android中使用OpenGL的方法有两种,一种是在native层使用EGL+OpenGL来实现,另一种则是GLSurfaceView。 本文将使用GLSurfaceView+MediaPlayer实现播放,并通过OpenGL进行简单的滤镜处理,以此来说明如何使用GLSurfaceView。

题外话:nativewindow和OpenGL渲染视频的代码,可以参考ijkplayer的实现。

OpenGL

OpenGL引擎渲染图像的流程比较复杂,简单来说是以下几步。(引用自https://www.jianshu.com/p/99daa25b4573) 但我们最主要先了解顶点处理阶段及片元处理阶段。

阶段一:指定几何对象 所谓几何对象,就是点,直线,三角形,这里将根据具体执行的指令绘制几何图元。比如,OpenGL提供给开发者的绘制方法glDrawArrays,这个方法里的第一个参数是mode,就是制定绘制方式,可选值有一下几种。

GL_POINT:以点的形式进行绘制,通常用在绘制粒子效果的场景中。 GL_LINES:以线的形式进行绘制,通常用在绘制直线的场景中。 GL_TRIANGLE_STRIP:以三角形的形式进行绘制,所有二维图像的渲染都会使用这种方式。

阶段二:顶点处理 不论以上的几何对象是如何指定的,所有的几何数据都将会经过这个阶段。这个阶段所做的操作就是,根据模型视图和投影矩阵进行变换来改变顶点的位置,根据纹理坐标与纹理矩阵来改变纹理坐标的位置,如果涉及三维的渲染,那么这里还要处理光照计算与法线变换。 一般输出是以gl_Position来表示具体的顶点位置的,如果是以点来绘制几何图元,那么还应该输出gl_PointSize。

阶段三:图元组装 在经过阶段二的顶点处理操作之后,还是纹理坐标都是已经确定好了的。在这个阶段,顶点将会根据应用程序送往图元的规则(如GL_POINT、GL_TRIANGLE_STRIP),将纹理组装成图元。

阶段四:栅格化操作 由阶段三传递过来的图元数据,在此将会分解成更小的单元并对应于帧缓冲区的各个像素。这些单元称为片元,一个片元可能包含窗口颜色、纹理坐标等属性。片元的属性是根据顶点坐标利用插值来确定的,这就是栅格化操作,也就是确认好每一个片元是什么。

阶段五:片元处理 通过纹理坐标取得纹理(texture)中相对应的片元像素值(texel),根据自己的业务处理(比如提亮、饱和度调节、对比度调节、高斯模糊)来变换这个片元的颜色。这里的输出是gl_FragColor,用于表示修改之后的像素的最终结果。

阶段六:帧缓冲操作 该阶段主要执行帧缓冲的写入操作,这也是渲染管线的最后一步,负责将最终的像素值写入到帧缓冲区中。

OpenGL ES提供了可编程的着色器来代替渲染管线的某个阶段。 Vertex Shader(顶点着色器)用来替代顶点处理阶段。 Fragment Shader(片元着色器,又称为像素着色器)用来替换片元处理阶段。

简单来讲就是OpenGL会在顶点着色器确定顶点的位置,然后这些顶点连起来就是我们想要的图形。接着在片元着色器里面给这些图形上色:

在这里插入图片描述

GLSurfaceView

GLSurfaceView看名字就是可以使用OpenGL的SurfaceView,也确实如此,它继承自SurfaceView,具备SurfaceView的特性,并加入了EGL的管理,它自带了一个GLThread绘制线程(EGLContext创建GL环境所在线程即为GL线程),绘制的工作直接通过OpenGL在绘制线程进行,不会阻塞主线程,绘制的结果输出到SurfaceView所提供的Surface上。 所以为什么我们不直接用surfaceView来进行播放呢?有以下两个好处:

通过GLSurfaceView进行视频渲染,可以使用GPU加速,相对于SurfaceView使用画布进行绘制,OpenGL的绘制关联到GPU,效率更高。可以定制render(渲染器),从而可以实现定制效果。 使用流程:

创建一个GLSurfaceView用来承载视频 ->设置render(实现OpenGL着色器代码) ->创建SurfaceTexture,绑定的外部Texture ->将SurfaceTexture的surface设置给MediaPlayer,启动播放 ->在render的onDrawFrame中更新Texture,绘制新画面。

其中,render是最核心部分。

1、创建GLSurfaceView glView = findViewById(R.id.surface_view); glView.setEGLContextClientVersion(2); MyGLRender glVideoRenderer = new MyGLRender();//创建renderer glView.setRenderer(glVideoRenderer);//设置renderer

创建GLSurfaceView后,设置其OpenGL版本为2.0,然后设置render。下面介绍MyGLRender。

2、创建render

render需要实现GLSurfaceView.Renderer的三个接口:

public interface Renderer { void onSurfaceCreated(GL10 var1, EGLConfig var2); void onSurfaceChanged(GL10 var1, int var2, int var3); void onDrawFrame(GL10 var1); }

onSurfaceCreated进行渲染程序的初始化,创建Surface,启动MediaPlayer

@Override public void onSurfaceCreated(GL10 gl10, EGLConfig eglConfig) { initGLProgram(); Surface surface = crateSurface(); // mediaplayer play try { mPlayer.setSurface(surface); mPlayer.prepare(); mPlayer.start(); } catch (IOException e) { e.printStackTrace(); } } 渲染程序的初始化

initGLProgram()中创建顶点着色器和片元着色器代码,一步步看:

顶点着色器

private final String VSH_CODE = "uniform mat4 uSTMatrix;\n"+ "attribute vec4 aPosition;\n"+ "attribute vec4 aTexCoord;\n"+ "varying vec2 vTexCoord;\n"+ "void main(){\n"+ "vTexCoord = (uSTMatrix*aTexCoord).xy;\n"+ "gl_Position = aPosition;\n"+ "}";

OpenGL会将每个顶点的坐标传递给顶点着色器,我们可以在这里改变顶点的位置。例如我们给每个顶点都加上一个偏移,就能实现整个图形的移动。

aPosition为顶点坐标,赋值给gl_Position ,表示物体位置,构成图元,可由外部传入。 aTexCoord为纹理坐标,纹理坐标描述纹理该如何在图元上贴图,可由外部传入。 vTexCoord为最终要传递给片元着色器的纹理坐标,为什么要在aTexCoord的基础上进行矩阵转换呢?这是因为计算机图像坐标与纹理坐标的表示是不一致的。如下图:

在这里插入图片描述 在这里插入图片描述 因为我们使用的texture是从外部得到的,其对应的是计算机坐标系,所以需要矩阵转换,这个矩阵可通过SurfaceTexture.getTransformMatrix函数获取到。

片元着色器

private final String FSH_CODE = "#extension GL_OES_EGL_image_external : require\n"+ "precision mediump float;\n"+ "varying vec2 vTexCoord;\n"+ "uniform mat4 uColorMatrix;\n"+ "uniform samplerExternalOES sTexture;\n"+ "void main() {\n"+ "gl_FragColor=uColorMatrix*texture2D(sTexture, vTexCoord).rgba;\n"+ //"gl_FragColor = texture2D(sTexture, vTexCoord);\n"+ "}";

片元着色器要注意的是#extension GL_OES_EGL_image_external : require,因为使用的是外部纹理samplerExternalOES类型的纹理sTexture,所以需要加上。 vTexCoord是从顶点着色器传过来的纹理坐标。 texture2D函数可以从该坐标获取到对应的颜色,这里我们加入了颜色转换矩阵uColorMatrix,这样就能进行一些效果处理。最后将颜色赋值给gl_FragColor。

颜色效果矩阵如下:

private static float[] COLOR_MATRIX3 = { // 怀旧效果矩阵 0.393f,0.349f, 0.272f,0.0f , 0.769f,0.686f,0.534f,0.0f, 0.189f,0.168f,0.131f,0.0f, 0.0f,0.0f,0.0f,1.0f };

创建渲染程序 如何将两个着色器代码替换到渲染管线中呢,基本流程如下图: 在这里插入图片描述 编译shader程序(compileShader代码)

glCreateShader创建shader,参数为类型,指定顶点着色器还是片元着色器;glShaderSource加载shader代码;glCompileShader编译代码,并glGetShaderiv通过GL_COMPILE_STATUS获取编译是否正确;得到一个shader程序的ID。

创建渲染程序(buildProgram代码)

glCreateProgram创建program;glAttachShader通过shader程序的ID,把shader程序附进来;glLinkProgram链接程序,并glGetProgramiv通过GL_LINK_STATUS获取链接是否正确。得到一个渲染程序的ID。

最后调用glUseProgram,传入渲染程序的ID就可以了。

代码如下:

//创建shader private int compileShader(int type, String code){ int shaderObjectId = GLES20.glCreateShader(type); if (shaderObjectId == 0){ Log.d(TAG, "compileShader: glCreateShader err"); return 0; } GLES20.glShaderSource(shaderObjectId, code); GLES20.glCompileShader(shaderObjectId); int[] compileStatus = new int[1]; GLES20.glGetShaderiv(shaderObjectId, GLES20.GL_COMPILE_STATUS, compileStatus, 0); if (compileStatus[0] == 0){ // if it failed, delete the shader object Log.d(TAG, "compileShader: glCompileShader err"); GLES20.glDeleteShader(shaderObjectId); return 0; } Log.d(TAG, "compileShader: success: "+shaderObjectId); return shaderObjectId; } //创建渲染程序 private int buildProgram(int vertexShaderId, int fragmentShaderId){ int programObjectId = GLES20.glCreateProgram(); if(programObjectId == 0){ Log.d(TAG, "buildProgram: glCreateProgram err"); return 0; } GLES20.glAttachShader(programObjectId, vertexShaderId); GLES20.glAttachShader(programObjectId, fragmentShaderId); GLES20.glLinkProgram(programObjectId); int[] linkStatus = new int[1]; GLES20.glGetProgramiv(programObjectId, GLES20.GL_LINK_STATUS, linkStatus, 0); if (linkStatus[0] == 0){ // if it failed, delete the shader object GLES20.glDeleteProgram(programObjectId); Log.d(TAG, "buildProgram: glLinkProgram err"); return 0; } Log.d(TAG, "buildProgram: success: "+programObjectId); return programObjectId; }

填充顶点坐标及纹理坐标 完成顶点着色器及片元着色器后,创建渲染程序,接下来我们要填充顶点信息: 顶点着色器中,aPosition表示物体位置坐标,坐标系中x轴从左到右是从-1到1变化的,y轴从下到上是从-1到1变化的,物体的中心点恰好是(0,0)的位置。 在这里插入图片描述 aTexCoord描述纹理坐标(如上图OpenGL二维纹理坐标),我们现在要把纹理按照,左下->右下->左上->右上的顺序,贴到物体上。所以对应的顶点坐标及纹理坐标数据为:

//顶点着色器坐标,z为0 float[] vers = { -1.0f, -1.0f, 0.0f, 1.0f, -1.0f, 0.0f, -1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, }; //纹理坐标,texture坐标ST,需要根据图像进行转换 float[] txts = { 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f };

通过 GLES20.glEnableVertexAttribArray及GLES20.glVertexAttribPointer两个函数,完成顶点信息设置。

设置颜色效果 通过glGetUniformLocation获取到uColorMatrix矩阵的句柄,将颜色矩阵设赋值给它就行。这样就会在片元着色器中生效。

//设置颜色效果 int colorMatrixHandle = GLES20.glGetUniformLocation(programId, "uColorMatrix"); GLES20.glUniformMatrix4fv(colorMatrixHandle, 1, false, COLOR_MATRIX3, 0);

完整代码:

private void initGLProgram(){ int vertexShader = compileShader(GLES20.GL_VERTEX_SHADER, VSH_CODE); int fragmentShader = compileShader(GLES20.GL_FRAGMENT_SHADER, FSH_CODE); int programId = buildProgram(vertexShader, fragmentShader); if(programId == 0) return; GLES20.glUseProgram(programId); mSTMatrixHandle = GLES20.glGetUniformLocation(programId, "uSTMatrix");//转换矩阵 //顶点着色器坐标 float[] vers = { -1.0f, -1.0f, 0.0f, 1.0f, -1.0f, 0.0f, -1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, }; FloatBuffer vertexBuffer = ByteBuffer.allocateDirect(vers.length * 4) .order(ByteOrder.nativeOrder()) .asFloatBuffer() .put(vers); vertexBuffer.position(0); //纹理坐标,texture坐标ST,需要根据图像进行转换 float[] txts = { 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f }; FloatBuffer textureVertexBuffer = ByteBuffer.allocateDirect(txts.length * 4) .order(ByteOrder.nativeOrder()) .asFloatBuffer() .put(txts); textureVertexBuffer.position(0); //设置顶点坐标和纹理坐标 int apos = GLES20.glGetAttribLocation(programId, "aPosition"); GLES20.glEnableVertexAttribArray(apos); GLES20.glVertexAttribPointer(apos, 3, GLES20.GL_FLOAT, false, 12, vertexBuffer); int atex = GLES20.glGetAttribLocation(programId, "aTexCoord"); GLES20.glEnableVertexAttribArray(atex); GLES20.glVertexAttribPointer(atex, 2, GLES20.GL_FLOAT, false, 8, textureVertexBuffer); //设置颜色效果 int colorMatrixHandle = GLES20.glGetUniformLocation(programId, "uColorMatrix"); GLES20.glUniformMatrix4fv(colorMatrixHandle, 1, false, COLOR_MATRIX3, 0); } 3、创建SurfaceTexture,绑定外部纹理

glGenTextures创建Texture,我们使用的是外部纹理,所以只需要一个即可。 glBindTexture绑定纹理,要注意这里需要设置GL_TEXTURE_EXTERNAL_OES标志。 glTexParameterf设置一些属性,这里设置的是缩放的算法。 然后根据mTextureID创建SurfaceTexture,然后创建Surface,Surface就可以设置给MeidaPlayer。

完整代码:

private Surface crateSurface(){ // Create SurfaceTexture that will feed this textureId and pass to MediaPlayer int[] textures = new int[1];//just one texures,use external mode GLES20.glGenTextures(1, textures, 0); mTextureID = textures[0]; GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, mTextureID); GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST); GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); mSurfaceTexture = new SurfaceTexture(mTextureID); mSurfaceTexture.setOnFrameAvailableListener(this); Surface surface = new Surface(mSurfaceTexture); return surface; } 4、Surface设置给MediaPlayer,启动播放

没什么可以说道的,就是把上面创建的surface设置给播放器,同步的prepare,加上start。

// mediaplayer play try { mPlayer.setSurface(surface); mPlayer.prepare(); mPlayer.start(); } catch (IOException e) { e.printStackTrace(); } 5、onDrawFrame中更新Texture,绘制新画面

上面创SurfaceTexture时通过setOnFrameAvailableListener设置了监听器,监听纹理的更新,更新了,我们就设置isFrameUpdate为true。 onDrawFrame是render进行绘制时会调用,当isFrameUpdate为true,意味着我们可以进行绘制了。

先通过SurfaceTexture.updateTexImage()更新纹理,然后glViewport设置绘制的窗口大小。

OpenGL虽然是在Surface上绘制,但我们可以不铺满整个Surface,可以只在它的某部分绘制,例如我们可以用下面代码只用TextureSurface的左下角的四分之一去显示OpenGL的画面:

//width、height是TextureView的宽高 GLES20.glViewport(0, 0, width/2, height/2);

在这里插入图片描述

我们这里还是铺满整个View,宽高可以在onSurfaceChanged中获取到。

绘制前先清除上一帧,

//clear GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f); GLES20.glClear(GLES20.GL_DEPTH_BUFFER_BIT | GLES20.GL_COLOR_BUFFER_BIT);

当然这里还可以再清空片元着色器的外部纹理。

设置纹理变换矩阵,矩阵在SurfaceTexture.getTransformMatrix获取到 激活绑定纹理,然后就可以绘制了。 绘制采用的三角形方式GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);

render缺省模式是 RENDERMODE_CONTINUOUSLY,就是说 surface绘制线程不停循环调用onDrawFrame。所以帧频控制取决于每帧的绘制时间,通常都是在onDrawFrame里加延时来控制的。 当设置为RENDERMODE_WHEN_DIRTY时,就是通常的事件驱动模式来绘制。画面重新显示出来或 requestRender()时才会调用onDrawFrame.

完整代码如下:

@Override public void onSurfaceChanged(GL10 gl10, int width, int height) { screenWidth = width; screenHeight = height; } @Override public void onDrawFrame(GL10 gl10) { synchronized (this){ if(isFrameUpdate){ mSurfaceTexture.updateTexImage(); mSurfaceTexture.getTransformMatrix(mSTMatrix); isFrameUpdate = false; } } //update width and height GLES20.glViewport(0, 0, screenWidth, screenHeight); //clear GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f); GLES20.glClear(GLES20.GL_DEPTH_BUFFER_BIT | GLES20.GL_COLOR_BUFFER_BIT); //update st mat4 GLES20.glUniformMatrix4fv(mSTMatrixHandle, 1, false, mSTMatrix, 0); //bind and active, juest one time { GLES20.glActiveTexture(GLES20.GL_TEXTURE0); GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, mTextureID); } //draw GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); } @Override public void onFrameAvailable(SurfaceTexture surfaceTexture) { isFrameUpdate = true; } 总结

播放效果如下: 在这里插入图片描述 下一章会描述如何在native层使用EGL和OpenGL,这样会对Android OpenGL ES视频渲染有更深入的了解。



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3